package com.shigi.knapsack.solvers.ga;

import com.shigi.knapsack.KnapsackInstance;
import com.shigi.knapsack.KnapsackItem;
import com.shigi.knapsack.KnapsackSolution;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * Created by Miroslav Šiagi on 01/12/14.
 */
public class GAMain {

    private GAConfig gaConfig;
    private List<KnapsackPopulation> knapsackPopulations;
    private double avgRelativeError;
    private List<Double> avgRelativeErrors;

    public GAMain(GAConfig gaConfig, List<KnapsackInstance> knapsackInstances, List<KnapsackSolution> knapsackSolutions) {
        this.gaConfig = gaConfig;
        this.knapsackPopulations = new ArrayList<KnapsackPopulation>();
        this.avgRelativeErrors = new ArrayList<Double>();
        createPopulations(knapsackInstances, knapsackSolutions);
        execute();
    }

    public void execute() {
        for(KnapsackPopulation knapsackPopulation : knapsackPopulations) {
            //KnapsackPopulation originalPopulation = knapsackPopulation;
            for (int i = 0; i < gaConfig.getNumOfIterations(); i++) {
                //KnapsackPopulation copiedPopulation = new KnapsackPopulation(originalPopulation);
                runGAOnPopulation(knapsackPopulation);
            }
        }

        double sumOfErrors = 0.0;
        for (Double dbl : avgRelativeErrors) {
            sumOfErrors += dbl;
        }
        this.avgRelativeError = sumOfErrors / knapsackPopulations.size();
    }

    private void runGAOnPopulation(KnapsackPopulation knapsackPopulation) {
        int maxGenerations = gaConfig.getNumOfGenerations();
        int correctSolution = knapsackPopulation.getCorrectSolution();
        int generationsCounter = 0;
        List<Double> fitnessValues = new ArrayList<Double>();
        List<Double> relatErrors = new ArrayList<Double>();
        double bestFitnessEver = 0.0;
        int idOfBestPopulation = 0;
        double currentBestFitnessValue = GAUtils.computeFitnessOnPopulation(knapsackPopulation);
        fitnessValues.add(currentBestFitnessValue);

        while(doNotTerminate(maxGenerations, correctSolution, generationsCounter, currentBestFitnessValue)) {
            Arrays.sort(knapsackPopulation.getKnapsackIndividuals(), new Comparator<KnapsackIndividual>() {
                @Override
                public int compare(KnapsackIndividual o1, KnapsackIndividual o2) {
                    if (o1.getFitnessValue() < o2.getFitnessValue()) {
                        return 1;
                    } else if (o1.getFitnessValue() == o2.getFitnessValue()) {
                        return 0;
                    } else
                        return -1;
                }
            });
            KnapsackPopulation newPopulation =
                    new KnapsackPopulation(knapsackPopulation.getNumberOfPopulation(), knapsackPopulation.getNumberOfPopulation(), knapsackPopulation.getKnapsackIndividuals().length, knapsackPopulation.getCorrectSolution(), knapsackPopulation.getCapacity());
            int numberOfNewIndividuals = 0;

            int numberOfOldIndividuals = knapsackPopulation.getKnapsackIndividuals().length;
            while(numberOfNewIndividuals < numberOfOldIndividuals) {
                numberOfNewIndividuals = performReproductionOrCrossover(knapsackPopulation, newPopulation, numberOfNewIndividuals);

                int difference = numberOfOldIndividuals - numberOfNewIndividuals;
                if(difference < 3 && difference > 0) {
                    GAUtils.performMutation(newPopulation, gaConfig);
                    // elitism
                    for (int i = 0; i < difference; i++) {
                        newPopulation.getKnapsackIndividuals()[numberOfNewIndividuals++] = knapsackPopulation.getKnapsackIndividuals()[i];
                    }
                }

            }

            knapsackPopulation = newPopulation;
            knapsackPopulation.setNumberOfPopulation(++generationsCounter);
            currentBestFitnessValue = GAUtils.computeFitnessOnPopulation(knapsackPopulation);
            fitnessValues.add(currentBestFitnessValue);
            if(currentBestFitnessValue >= bestFitnessEver) {
                bestFitnessEver = currentBestFitnessValue;
                idOfBestPopulation = generationsCounter - 1;
            }

            relatErrors.add((knapsackPopulation.getCorrectSolution() - bestFitnessEver) / knapsackPopulation.getCorrectSolution());
        }

        for (int i = 0; i < maxGenerations; i++) {
            System.out.println(i + "\t" + relatErrors.get(i));
        }
        this.avgRelativeErrors.add((double) (knapsackPopulation.getCorrectSolution() - bestFitnessEver) / knapsackPopulation.getCorrectSolution());
    }

    private int performReproductionOrCrossover(KnapsackPopulation knapsackPopulation, KnapsackPopulation newPopulation, int numberOfNewIndividuals) {
        double rndNumber = Math.random();
        if(rndNumber <= gaConfig.getProbOfReproduction()) {
            KnapsackIndividual reproducedIndividual = GAUtils.performReproduction(knapsackPopulation, gaConfig);
            newPopulation.getKnapsackIndividuals()[numberOfNewIndividuals++] = reproducedIndividual;
        } else {
            KnapsackIndividual firstParent = GAUtils.performLinearRankSelection(knapsackPopulation, gaConfig);
            KnapsackIndividual secondParent = GAUtils.performLinearRankSelection(knapsackPopulation, gaConfig);
            numberOfNewIndividuals = GAUtils.performCrossover(firstParent, secondParent, newPopulation, numberOfNewIndividuals);
        }

        return numberOfNewIndividuals;
    }

    private boolean doNotTerminate(int maxGenerations, int correctSolution, int generationsCounter, double currentBestFitnessValue) {
        return (maxGenerations > generationsCounter && correctSolution > currentBestFitnessValue);
    }

    private void createPopulations(List<KnapsackInstance> knapsackInstances, List<KnapsackSolution> knapsackSolutions) {
        for(KnapsackInstance knapsackInstanceIter : knapsackInstances) {
            for(KnapsackSolution knapsackSolutionIter : knapsackSolutions) {

                if(knapsackInstanceIter.getId() == knapsackSolutionIter.getId()) {
                    addPopulation(knapsackInstanceIter, knapsackSolutionIter);
                }
            }
        }
    }

    private void addPopulation(KnapsackInstance knapsackInstance, KnapsackSolution knapsackSolution) {
        int numberOfItems = knapsackInstance.getNumberOfItems();
        KnapsackPopulation newKnapsackPopulation = new KnapsackPopulation(knapsackInstance.getId(), 0, numberOfItems, knapsackSolution.getTotalValue(), knapsackInstance.getTotalWeight());

        int numberOfIndividuals = gaConfig.getNumOfIndividuals();
        KnapsackIndividual[] knapsackIndividuals = new KnapsackIndividual[numberOfIndividuals];

        for (int i = 0; i < numberOfIndividuals; i++) {

            int capacity = knapsackInstance.getTotalWeight();
            int sumOfWeights  = knapsackInstance.getSumOfWeights();
            KnapsackItem[] knapsackItems = knapsackInstance.getKnapsackItems();

            KnapsackItem[] newKnapsackItems = new KnapsackItem[numberOfItems];
            for(int j = 0; j < numberOfItems; j++) {
                KnapsackItem newKnapsackItem = new KnapsackItem(j);
                newKnapsackItem.setValue(knapsackItems[j].getValue());
                newKnapsackItem.setWeight(knapsackItems[j].getWeight());

                double propOfUsedItem = (double) capacity / (sumOfWeights);
                newKnapsackItem.setUsed(Math.random() <= propOfUsedItem);
                newKnapsackItems[j] = newKnapsackItem;
            }

            knapsackIndividuals[i] = new KnapsackIndividual(numberOfItems);
            knapsackIndividuals[i].setKnapsackItems(newKnapsackItems);

        }

        newKnapsackPopulation.setKnapsackIndividuals(knapsackIndividuals);
        this.knapsackPopulations.add(newKnapsackPopulation);
    }

}
